package com.google.gautoard;

import com.google.appengine.api.datastore.AsyncDatastoreService;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.memcache.AsyncMemcacheService;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceFactory;

import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This Bean provides a way to marshall/unmarshall a string Event (that arduino/raspberrypi
 * understands) to a java event (that can be stored in memcache/datastore/objectify).
 *
 * @author dras
 */
public class EventBean {

  boolean parsedflag = false;

  /**
   * A domain to differentiate sensors
   */
  public String domain;

  /**
   * The user that controls the sensor
   */
  public String user;

  /**
   * The location of the sensor
   */
  public String location;

  /**
   * The device type
   */
  public String device;

  /**
   * The parameter the device is setting/getting
   */
  public String parameter;
  private String action;

  /**
   * The value of the device's reading
   */
  public String value;
  private long timestamp;
  private String ukey;
  private String rawevent;
  AsyncDatastoreService asyncdatastore = DatastoreServiceFactory.getAsyncDatastoreService();
  DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
  private static final Logger log = Logger.getLogger(EventBean.class.getName());
  MemcacheService syncCache = MemcacheServiceFactory.getMemcacheService();
  AsyncMemcacheService asyncCache = MemcacheServiceFactory.getAsyncMemcacheService();

  /**
   * Models the key/value pair
   */
  public EventBean() {}

  /**
   * Creates an EventBean from the key/value pair
   * @param s The key/value pair
   */
  public EventBean(String s) {
    this.rawevent = s;
  }

  public String getDomain() {
    if (!parsedflag) {
      parse();
    }
    return domain;
  }

  public void setDomain(String domain) {
    this.domain = domain;
  }

  public String getUser() {
    if (!parsedflag) {
      parse();
    }
    return user;
  }

  public void setUser(String user) {
    this.user = user;
  }

  public String getLocation() {
    if (!parsedflag) {
      parse();
    }
    return location;
  }

  public void setLocation(String location) {
    this.location = location;
  }

  public String getDevice() {
    if (!parsedflag) {
      parse();
    }
    return device;
  }

  public void setDevice(String device) {
    this.device = device;
  }

  public String getParameter() {
    if (!parsedflag) {
      parse();
    }
    return parameter;
  }

  public void setParameter(String parameter) {
    this.parameter = parameter;
  }

  public String getAction() {
    if (!parsedflag) {
      parse();
    }
    return action;
  }

  public void setAction(String action) {
    this.action = action;
  }

  public String getValue() {
    if (!parsedflag) {
      parse();
    }
    if (null == value || "".equals(value)) {
      log.log(Level.INFO, "gotta grab a val for " + this.getUniqueKey());
      this.load();
      log.log(Level.INFO, "got a val:" + value);
    }
    return value;
  }

  /**
   * Returns the sensor value as a double
   * @return double sensor value
   */
  public double getValueDouble() {
    return Double.parseDouble(value);
  }

  public void setValue(String value) {
    this.value = value;
  }

  public Long getTimestamp() {
    if (!parsedflag) {
      parse();
    }
    return timestamp;
  }

  public Date getDatestamp() {
    return new Date(timestamp * 1000);
  }

  public void setTimestamp(Long timestamp) {
    this.timestamp = timestamp;
  }

  /**
   * The key portion of the entry
   * @return the key
   */
  public String getUniqueKey() {
    if (!parsedflag) {
      parse();
    }
    if (null == ukey || "".equals(ukey)) {
      ukey = domain + "." + user + "." + location + "." + device + "." + parameter;
    }
    return ukey;
  }

  /**
   * The value and timestamp of the entry
   * @return the value;timestamp of the entry
   */
  public String getValuets() { // gets the value + timestamp
    if (!parsedflag) {
      parse();
    }
    return value + ";" + timestamp;
  }

  /**
   * Sets the value;timestamp of the entry
   * @param s a String with the value;timestamp of the entry
   */
  public void setValuets(String s) { // sets the value + timestamp
    String[] stsecond = s.split(";");
    if (stsecond.length == 1) {
      value = stsecond[0];
      // if no timestamp was given, use current time
      timestamp = System.currentTimeMillis() / 1000;
    } else if (stsecond.length == 2) {
      timestamp = Long.parseLong(stsecond[1]);
    }
  }

  /**
   *
   * @return true if this is an update action (stored in log)
   * TODO: should this be isLog() instead?
   */
  public boolean isUpdate() {
    if (!parsedflag) {
      parse();
    }
    return "update".equals(action);
  }

  /**
   *
   * @return true if this is a set action (stored in store)
   */
  public boolean isASet() {
    if (!parsedflag) {
      parse();
    }
    return "set".equals(action);
  }

  /**
   *
   * @return true if this is a get action (retrieve value from store)
   */
  public boolean isAGet() {
    if (!parsedflag) {
      parse();
    }
    return "get".equals(action);
  }

  /**
   *
   * @return true if this is a listen action (wait for value in store to change)
   */
  public boolean isAListen() {
    if (!parsedflag) {
      parse();
    }
    return "listen".equals(action);
  }

  @Override
  public String toString() {
    if (!parsedflag) {
      parse();
    }
    return getUniqueKey() + "=" + getValuets();
  }

  /**
   * Compares two EventBeans for equality
   * @param eb to compare
   * @return true if the two EventBeans have the same unique key
   */
  public boolean equalsKey(EventBean eb) {
    if (!parsedflag) {
      parse();
    }
    if (this.getUniqueKey().equals(eb.getUniqueKey())) {
      return true;
    }
    return false;
  }

  /**
   * Store the EventBean in the log (including caching)
   */
  public void log() {
    if (!parsedflag) {
      parse();
    }

    // update datastore
    Key k = KeyFactory.createKey("log", this.getDomain() + "." + this.getTimestamp());
    Entity tolog = new Entity(k);
    tolog.setProperty("key", this.getUniqueKey());
    tolog.setProperty("value", this.getValue());
    Date d = new Date(this.getTimestamp() * 1000);
    tolog.setProperty("time", d);
    asyncdatastore.put(tolog);
    store();
  }

  /**
   * Store the EventBean in the store (including caching)
   */
  public void store() {
    if (!parsedflag) {
      parse();
    }
    // update memcache
    asyncCache.put(this.getUniqueKey(), this.getValuets());

    // update datastore
    Key k = KeyFactory.createKey("store", this.getUniqueKey());
    Entity tostore = new Entity(k);
    tostore.setProperty("value", this.getValue());
    Date d = new Date(this.getTimestamp() * 1000);
    tostore.setProperty("time", d);
    asyncdatastore.put(tostore);
  }

  /**
   * Loads the EventBean represented by the unique key (from cache, ifnotexists then from the Datastore)
   */
  public void load() {
    if (!parsedflag) {
      parse();
    }
    // check memcache first
    String valuets = (String) syncCache.get(this.getUniqueKey());
    log.log(Level.INFO, "cache load got: {0} for {1}", new Object[] {valuets, this.getUniqueKey()});
    if (valuets == null) {
      // the cache doesn't have that key or it's value is null
      if (!syncCache.contains(this.getUniqueKey())) {
        // the cache doesn't have that key
        Key k = KeyFactory.createKey("store", this.getUniqueKey());
        try {
          Entity toload = datastore.get(k);
          this.setValue((String) toload.getProperty("value"));
          Date d = (Date) toload.getProperty("time");
          this.setTimestamp(d.getTime() / 1000L);
          // store the freshly loaded key in the memcache
          asyncCache.put(this.getUniqueKey(), this.getValuets());
        } catch (EntityNotFoundException enfe) {
          log.log(Level.SEVERE, "entity not found: {0}", this.getUniqueKey());
        }
      } else {
        // it's value is null
        syncCache.delete(this.getUniqueKey());
      }
    } else {
      // key found in cache
      String[] stsecond = valuets.split("\\;");
      setValue(stsecond[0]);
      setTimestamp(Long.parseLong(stsecond[1]));
    }
  }

  /**
   * Parses a raw String into component fields of this EventBean
   */
  public void parse() {
    if (parsedflag) {
      return;
    }
    // log.log(Level.INFO, "parsing {0}", rawevent);
    // split on the equals (if present)
    String[] staparsed = rawevent.split("=");

    // chop the originator up
    String[] stfirst = staparsed[0].split("\\.");
    if (stfirst.length < 6) {
      log.log(Level.WARNING, "could not parse, only found {0} fields", stfirst.length);
      parsedflag = false;
      return;
    }
    setDomain(stfirst[0] + "." + stfirst[1]);
    setUser(stfirst[2]);
    setLocation(stfirst[3]);
    setDevice(stfirst[4]);
    setParameter(stfirst[5]);
    if (stfirst.length > 6) {
      setAction(stfirst[6]);
    }

    // chop the 2nd half up
    if (staparsed.length == 2) {
      // gets do not have a value, only set and update
      String[] stsecond = staparsed[1].split("\\;");
      setValue(stsecond[0]);

      if (stsecond.length == 1) {
        // if no timestamp was given, use current time
        setTimestamp(System.currentTimeMillis() / 1000);
      } else if (stsecond.length == 2) {
        setTimestamp(Long.parseLong(stsecond[1]));
      }
    }
    parsedflag = true;
  }
}
